package com.fry.expandablelistviewexdemo;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnGroupCollapseListener;
import android.widget.ExpandableListView.OnGroupExpandListener;

public class ExpandableListViewEx 
	extends ExpandableListView
	implements OnGroupExpandListener, 
	OnGroupCollapseListener,
	OnScrollListener {
	
	public static final int PINNED_HEADER_GONE = 0;

	public static final int PINNED_HEADER_VISIBLE = 1;

	public static final int PINNED_HEADER_PUSHED_UP = 2;
     
	public interface PinnedHeaderAdapter {
		
        void configurePinnedHeader(View header, long packedPosition);
    }
	
	private PinnedHeaderAdapter mAdapter;
	
    private View mHeaderView;
    private boolean mHeaderViewVisible;
    private int mHeaderViewWidth;
    private int mHeaderViewHeight;
    
    private OnGroupExpandListener mOnGroupExpandListener;
    private OnGroupCollapseListener mOnGroupCollapseListener;
    private OnScrollListener mOnScrollListener;
    
    private boolean mHasExpand;
	private int mExpandIndex;
	
	private int mPushedY;
	
	private LayoutInflater mInflater;

    public ExpandableListViewEx(Context context) {
        super(context);
        setupSomeListeners();
        initHeader(context, null);
    }

    public ExpandableListViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        setupSomeListeners();
        initHeader(context, attrs);
    }

    public ExpandableListViewEx(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setupSomeListeners();
        initHeader(context, attrs);
    }
    
    private void initHeader(Context context, AttributeSet attrs) {
    	TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExpandableListViewEx);
    	int headerLayout = a.getResourceId(R.styleable.ExpandableListViewEx_headerLayout, -1);
    	if(headerLayout > 0) {
    		setPinnedHeaderView(headerLayout);
    	}
    }
    
    private void setupSomeListeners() {
    	super.setOnGroupCollapseListener(this);
        super.setOnGroupExpandListener(this);
        super.setOnScrollListener(this);
    }

    private void setPinnedHeaderView(View view) {
        mHeaderView = view;

        if (mHeaderView != null) {
            setFadingEdgeLength(0);
            mHeaderView.setOnClickListener(new OnClickListener() {
				
				@Override
				public void onClick(View v) {
					// see LinearLayoutEx's post for detail, just do the thing in View's post here.
					post(new Runnable() {
						@Override
						public void run() {
							collapseGroup(mExpandIndex);
						}
					});
				}
			});
        }
        requestLayout();
    }
    
    public void setPinnedHeaderView(int res) {
        if(mInflater == null) {
        	mInflater = LayoutInflater.from(getContext());
        }
        View v = mInflater.inflate(res, this, false);
        setPinnedHeaderView(v);
    }
    
    @Override
    public void setAdapter(ExpandableListAdapter adapter) {
        super.setAdapter(adapter);
        mAdapter = (PinnedHeaderAdapter)adapter;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mHeaderView != null) {
            measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
            mHeaderViewWidth = mHeaderView.getMeasuredWidth();
            mHeaderViewHeight = mHeaderView.getMeasuredHeight();
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mHeaderView != null) {
            mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
            configureHeaderView();
        }
    }
    
    @Override
    public void setOnGroupExpandListener(
            OnGroupExpandListener onGroupExpandListener) {
        mOnGroupExpandListener = onGroupExpandListener;
    }
    
    @Override
    public void setOnGroupCollapseListener(
            OnGroupCollapseListener onGroupCollapseListener) {
        mOnGroupCollapseListener = onGroupCollapseListener;
    }
    
    private int computeState(long packedPosition, int flatPosition) {
    	int state = PINNED_HEADER_GONE;
    	if(mHasExpand) {
    		int groupPos = ExpandableListViewEx.getPackedPositionGroup(packedPosition);
    		
    		// a bug of google?
     		if(groupPos == -1 && getHeaderViewsCount() == 0 && flatPosition == 0) {
     			groupPos = 0;
     		}
    		if (mExpandIndex == groupPos) {
    			int type = ExpandableListViewEx.getPackedPositionType(packedPosition);
        		View firstView = getChildAt(0);
        		int topY = firstView != null ? firstView.getTop() : 0;
        		
        		boolean shouldPushUp = false;
        		long nextGroupPos = ExpandableListViewEx.getPackedPositionForGroup(groupPos + 1);
        		int flatNextGroupPos = getFlatListPosition(nextGroupPos);
        		int childCnt = getChildCount();
        		for (int i = 0; i < childCnt; i++) {
    				if(flatPosition + i == flatNextGroupPos) {
    					if(getChildAt(i).getTop() <= mHeaderViewHeight) {
    						mPushedY = getChildAt(i).getTop();
    						shouldPushUp = true;
    						break;
    					}
    				}
    			}
        		
    			if(shouldPushUp) {
    				state = PINNED_HEADER_PUSHED_UP;
    			} else if(type != ExpandableListViewEx.PACKED_POSITION_TYPE_GROUP) {
    				state = PINNED_HEADER_VISIBLE;
    			} else if(topY != 0) {
    				state = PINNED_HEADER_VISIBLE;
    			}
    		}
    	}
    	return state;
    }
    
    private void configureHeaderView() {
        if (mHeaderView == null) {
            return;
        }
        int flatPosition = getFirstVisiblePosition();
        long packedPosition = getExpandableListPosition(flatPosition);
        int state = computeState(packedPosition, flatPosition);
        switch (state) {
            case PINNED_HEADER_GONE: {
                mHeaderViewVisible = false;
                break;
            }

            case PINNED_HEADER_VISIBLE: {
                mAdapter.configurePinnedHeader(mHeaderView, packedPosition);
                if (mHeaderView.getTop() != 0) {
                    mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
                }
                mHeaderViewVisible = true;
                break;
            }

            case PINNED_HEADER_PUSHED_UP: {
                int y = mPushedY - mHeaderViewHeight;
                
                mAdapter.configurePinnedHeader(mHeaderView, packedPosition);
                if (mHeaderView.getTop() != y) {
                    mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
                }
                mHeaderViewVisible = true;
                break;
            }
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mHeaderViewVisible) {
            drawChild(canvas, mHeaderView, getDrawingTime());
        }
    }

	@Override
	public void onGroupCollapse(int groupPosition) {
		mHasExpand = false;
    	mExpandIndex = -1;
		configureHeaderView();
		if(mOnGroupCollapseListener != null) {
			mOnGroupCollapseListener.onGroupCollapse(groupPosition);
		}
	}

	@Override
	public void onGroupExpand(int groupPosition) {
		if(mHeaderView != null) {
			collapseGroupExcept(groupPosition);
	    	setSelectedGroup(groupPosition);
		}
    	mHasExpand = true;
    	mExpandIndex = groupPosition;
    	configureHeaderView();
    	if(mOnGroupExpandListener != null) {
    		mOnGroupExpandListener.onGroupExpand(groupPosition);
		}
	}
	
	private void collapseGroupExcept(int groupPosition) {
    	for (int i = 0; i < getExpandableListAdapter().getGroupCount(); i++) {
			if(i != groupPosition && isGroupExpanded(i)) {
				collapseGroup(i);
			}
		}
    }

	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		if(mOnScrollListener != null) {
			mOnScrollListener.onScrollStateChanged(view, scrollState);
		}
	}

	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		configureHeaderView();
		if(mOnScrollListener != null) {
			mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
		}
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		boolean process  = false;
		if(mHeaderView != null && mHeaderViewVisible) {
			Rect rect = new Rect();
			mHeaderView.getHitRect(rect);
			if(rect.contains((int)ev.getX(), (int)ev.getY())) {
				process = mHeaderView.dispatchTouchEvent(ev);
			}
		} 
		
		// for move event, if it start on header view, then there will be exception here, 
		// catching it is a acceptable approach
		try {
			if(!process){
				process =  super.dispatchTouchEvent(ev);
			}
		} catch (Exception e) {
			process = false;
		}
		
		return process;
	}
	
	static class SavedState extends BaseSavedState {
		
		private boolean mHasExpand;
		private int mExpandIndex;
        
		private SavedState(Parcelable superState, boolean hasExpand, int expandIndex) {
            super(superState);
            mHasExpand = hasExpand;
            mExpandIndex = expandIndex;
        }

        private SavedState(Parcel in) {
            super(in);
            mHasExpand = in.readInt() == 1;
            mExpandIndex = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(mHasExpand ? 1 : 0);
            out.writeInt(mExpandIndex);
        }

        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
	
	@Override
	public void onRestoreInstanceState(Parcelable state) {
		if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        
        mHasExpand = ss.mHasExpand;
        mExpandIndex = ss.mExpandIndex;
	}
	
	@Override
	public Parcelable onSaveInstanceState() {
		Parcelable superState =  super.onSaveInstanceState();
		SavedState state = new SavedState(superState, mHasExpand, mExpandIndex);
		return state;
	}
}
